<?php
/**
 * Created by PhpStorm.
 * User: lihui
 * Date: 3/26/2021
 * Time: 16:36
 */
namespace app\common\model\coin;

use app\common\model\base\A3Mall;
use app\common\model\Calculation;
use app\common\model\file\Association;
use app\common\model\order\OrderTake;

use app\common\traits\recordAllRun;
use mall\basic\Users;
use think\Exception;
use think\facade\Db;

class CoinRuleRunTrade extends A3Mall
{

    use recordAllRun;   //使用链表查询处理


    protected $name = "coin_rule_run_trade";

    protected $type = [
    ];


    // 请求第三方状态
    const REQUEST_STATUS_APPLY = 1; //第三方申请
    const REQUEST_STATUS_SUCCESS = 2; //第三方申请
    const REQUEST_STATUS_FAIL = 3; //第三方申请

    // 设置交易状态类型
    const STATUS_FAIL = 0;  //申请交易失败
    const STATUS_APPLY = 1;    //申请
    const STATUS_MATCH = 2; // 申请成功，进行撮合
    const STATUS_PARTIAL_FILLED = 3;  // 部分撮合成功；
    const STATUS_CANCELING = 4;  // 申请取消
    const STATUS_FILLED = 5;  // 全部撮合完成
    const STATUS_PARTIAL_CANCELED = 6;  // 部分撤销
    const STATUS_CANCELED = 7;  // 全部撤销

    /**
     * 状态条件类型列表
     */
    const STATUS_WHERE_TYPE_LIST = [
        'MONITOR'   => 'monitor',   //需要监控状态的订单
        'FINISH'   => 'finish',   //订单已完成状态【请求成功后】
    ];

    //类型
    const TYPE_BUY  = 1; //买入
    const TYPE_SELL = 2; //卖出

    const IS_JOIN_RUN_LIST = [
        'NULL'  => 0,   // 未查询
        'JOIN'  => 1,   // 加入到 run表中
        'NOT_JOIN'  => 2,   // 无需加入
    ];

    // 加入报表状态
    const JOIN_STATISTICS_LIST = [
        'NULL'  => 0,   //未处理
        'WAIT'  => 1,   //等待进入
        'JOIN'  => 2,   //已加入
        'NOT_JOIN'  => 3,   //亏本，无需加入
    ];


    private $unitDefault = 'USDT';  // 设置默认火币种类

    // 小数点位数
    protected $scale = 20;

    // 设置卖出手续率 【用于计算卖出】
    protected $sellFeeRate = 0.002;


    /** 获取关联所有数据的列表
     * @param array $whereTmp  条件
     * @param array $limit  查询数量
     * @param string $order 排序
     * @param array $fields 查询字段（暂缺）
     * @return array
     */
    public function recordAll($whereTmp = [],$limit = [],$orderTmp = [],$fieldsTmp= []) {

        return $this->recordAllRun([$whereTmp,$limit,$orderTmp,$fieldsTmp],[],[
            'run'   => [CoinRuleRun::getTable(),'run.id=' . static::getTable()  . '.coin_rule_run_id' , 'INNER'],
            ]);
    }
    
    /**
     * 首单添加
     * @param int $coinRuleRunId 执行中ID
     * @param $details  需要的额外详情，必须字段 first_amount、user_id、coin_rule_id、coin_id、coin_name、isDouble
     */
    public function firsterOrder(int $coinRuleRunId,array $details) {
        // 计算预留金额
        $reserved_amount = ceil(bcmul($details['reserved_price'],$details['reserved_quantity'],$this->scale));  //金额为向上取整

        // 计算本次买入金额
        $amount =  bcsub(bcmul($details['first_amount'],$details['isDouble']?2:1,$this->scale),$reserved_amount,0);

        /***************** 计算需要买入金额 ****************/
        if(in_array(bccomp($amount,0,0),[-1,0])){
            $amount = 0;
        } else if(bccomp($amount,$details['buy_total_min'],$this->scale) == -1) {
            $amount = $details['buy_total_min'];
        }
        /*************** 计算需要买入金额 **************/

        $details = array_merge($details,[
            'cover_times'      => 0,
            'type'      => static::TYPE_BUY,
            'amount'    => $amount,
            'coin_rule_run_id'  => $coinRuleRunId,
        ]);
        return $this->apply($details);
    }

    /**
     * 补仓操作
     * @param int $coinRuleRunId 执行中ID
     * @param $details  需要的额外详情，必须字段 first_amount、cover_times、user_id、coin_rule_id、coin_id、coin_name、isDouble
     */
    public function coverOrder(int $coinRuleRunId,array $details) {
        $details = array_merge($details,[
            'cover_times'      => $details['cover_times'],
            'type'      => static::TYPE_BUY,
            'amount'    => $details['first_amount'],
            'coin_rule_run_id'  => $coinRuleRunId,
        ]);
        if($details['cover_times'] > 0) {
            $details['amount'] = bcmul($details['amount'],pow(2,$details['cover_times']),$this->scale);
        }
        return $this->apply($details);
    }


    /**
     * 计算预留参数
     * @param $details 规则详情
     */
    private function calculationReserved($details) {
        $quantity = $price = 0;
        $coin = new CoinRule();
        $tradeRuleList = $coin::TRADE_RULE_LIST;
        // 判断是否需要做预留处理
        if($details['is_open'] != $coin::STATUS_OPEN || $details['type'] != $coin::TYPE_ONE || $details['trade_rule'] == $tradeRuleList['ROUTINE']) {
            return [$quantity,$price];
        }
        /*********** 参数计算 *************/
        // 首单金额
        $firstAmount = bcmul($details['first_amount'],$coin::isFirstDouble($details['isDouble'])?2:1,$this->scale);
        $sellPrice = bcsub($details['unit_price'],0,$details['sell_price_scale']);  // 卖出单价
        $countQuantity = $details['quantity'];  // 总数量
        $countAmount = bcmul($countQuantity,$sellPrice,$this->scale); //总价值

        $buyMin = $details['buy_total_min'];    // 最小买入金额
        if($details['sell_type'] == CoinRule::SELL_TYPE_LIST['MARKET']) {
            //市价卖
            $sellMin = bcdiv(ceil(bcmul(bcmul($details['sell_number_min'],100,$this->scale),$sellPrice,$this->scale)),100,2);
        } else {
            $sellMin = $details['sell_total_min'];   // 最小卖卖出金额
        }
        $sellMin = bcmul($sellMin,bcadd(1 , $this->sellFeeRate,3),3);

        // 计算卖出单价
        switch ($details['trade_rule']) {
            case $tradeRuleList['SAVE_FIRST']:  // 全额预留首单操作
                if(bccomp($countAmount,bcadd($firstAmount,$sellMin,$this->scale),$this->scale) != 1) {
                    break;
                }
                $price = $sellPrice;
                $quantity = bcdiv($firstAmount,$price,$this->scale);    //计算需要保留的数量
                //计算剩余卖出金额
                if(bccomp(bcmul(bcsub($countQuantity,$quantity,$details['sell_number_scale']),$sellPrice,$this->scale),$sellMin,$this->scale) == -1) {
                    // 判断保留首单之后是否还达到卖出最小单价
                    $quantity = $price = 0;
                    break;
                }
                break;
            case $tradeRuleList['SAVE_FIRST_MAX']:  // 最大限度预留保存处理
                if(bccomp($countAmount,$sellMin,$this->scale) != 1 || bccomp($countAmount,$buyMin,$this->scale) != 1) { // 判断买卖数量限制
                    break;
                }
                /******************** 计算最低卖出金额 ************************/
                $sellQuantity = bcdiv($sellMin,$sellPrice,$details['sell_number_scale']);
                if(bccomp($sellMin,bcmul($sellPrice,$sellQuantity,$this->scale),$this->scale) == 1) {
                    $sellQuantity = bcadd($sellQuantity,bcdiv(1,pow(10,$details['sell_number_scale']),$details['sell_number_scale']),$this->scale);
                }
                $sellAmount = bcmul($sellPrice,$sellQuantity,$this->scale);
                /****************** END 计算最低卖出金额 **********************/
                $saveAmount = bcsub($countAmount,$sellAmount,$this->scale); //保留金额

                if(bccomp($saveAmount,$firstAmount,$this->scale) != -1) {
                    // 保留金额大于等于下次买入金额【直接取下次买入金额】
                    $saveAmount = $firstAmount;
                } else {
                    // 保留金额小于下次买入金额处理【最常见】
                    if(bccomp($saveAmount,bcsub($firstAmount,$buyMin,$this->scale),$this->scale) == 1) {
                        // 下次买入金额必须大于首单金额 - 保留金额
                        $saveAmount = bcsub($firstAmount,$buyMin,$this->scale);
                    }
                }
                // 计算数量及单价
                $price = $sellPrice;
                $quantity = bcdiv($saveAmount,$price,$this->scale);    //计算需要保留的数量
                break;
        }
        return [$quantity,$price];
    }

    /**
     * 清仓操作
     * @param int $coinRuleRunId 执行中ID
     * @param $details  需要的额外详情，必须字段 first_amount、cover_times、user_id、coin_rule_id、coin_id、coin_name、isDouble
     */
    public function clearanceOrder(int $coinRuleRunId,array $details) {
        // 卖出数量添加上期未剩余数量
        $details['quantity'] = bcadd($details['quantity'],$details['last_remaining_quantity'],$this->scale);

        // 添加抵扣的数量
        $details['quantity'] = bcadd($details['quantity'],$details['deduct_fees'],$this->scale);

        // 添加偏差量
        $details['quantity'] = bcadd($details['quantity'],$details['deviations_quantity'],$this->scale);

        // 获取预留数量及单价
        list($details['reserved_quantity'],$details['reserved_price']) = $this->calculationReserved($details);

        // 卖出需要减去保留数量
        $details['quantity'] = bcsub($details['quantity'],$details['reserved_quantity'],$this->scale);

        $details['sell_api_quantity'] = bcsub($details['quantity'],0,$details['sell_number_scale']);

        $details = array_merge($details,[
            'cover_times'      => $details['cover_times'],
            'type'      => static::TYPE_SELL,
            'coin_rule_run_id'  => $coinRuleRunId,
        ]);

        if(!$insertDetails = $this->apply(array_merge($details,$details['sell_type'] == CoinRule::SELL_TYPE_LIST['MARKET']?['unit_price'=>0]:[]))) {
            return false;
        }
      /*  echo '<pre>';
        print_r($insertDetails);exit;
        // 清仓，结束执行状态，判断策略是否为多次策略，是则进行重新启动处理，存在则重新启动监控处理
        if(!(new CoinRuleRun())->closeMonitor($details['coin_rule_run_id'],$details['coin_rule_id'],$details['version'])) {
            return false;
        }*/

        return $insertDetails;
    }

    /**
     * 更新交易信息
     * @param int $id 交易ID
     * @param array $updateData 需更新的数据
     * @param $version  版本号
     */
    public function orderUpdate(int $id,array $updateData,$version) {

        if(!$this->recordUpdate([
            'id'    => $id,
            'version'   => $version
        ],$updateData)) {
            return false;
        }

        // 获取需要计算金额的状态
        if(array_key_exists('status',$updateData)) {
            switch($updateData['status']) {
                case static::STATUS_FILLED: //全部撮合完成
                case static::STATUS_PARTIAL_CANCELED:   //部分撤销
                    if(!$details = $this->alias('trade')
                        ->join('coin_rule_run run_details','run_details.id=trade.coin_rule_run_id','LEFT')
                        ->join('coin_rule rule','rule.id=trade.coin_rule_id','INNER')
                        ->join('users user','user.id=trade.user_id','INNER')
                        ->field(
                            [
                                'trade.id `trade-id`',
                                'trade.user_id `trade-user_id`',
                                'trade.type `trade-type`',
                                'trade.cover_times `trade-cover_times`',
                                'trade.field_amount `trade-field_amount`',
                                'trade.field_quantity `trade-field_quantity`',
                                'trade.field_price `trade-field_price`',
                                'trade.field_fees `trade-field_fees`',
                                'trade.version `trade-version`',
                                'trade.reserved_quantity `trade-reserved_quantity`',
                                'trade.reserved_price `trade-reserved_price`',
                                'trade.statistics `trade-statistics`',

                                'run_details.*',

                                'rule.type `rule-type`',
                                'rule.num `rule-num`',

                                'user.local_balance',
                                'user.local_consume',
                                'user.has_service_charge',

                            ])
                        ->find($id)->toArray()) {
                        return false;
                    }

                    /**************************** 判断修改执行状态信息 ***********************************/
                    //计算金额统计
                    $runDetails = [];

                    if($details['trade-type'] == static::TYPE_BUY) {    //买入处理

                        /************ 计算本次买入的平均金额及手续费（包含上次） **************/
                        $fieldAmount = bcmul($details['trade-reserved_quantity'],$details['trade-reserved_price'],$this->scale);
                        $fieldAmount = bcadd($fieldAmount,$details['trade-field_amount'],$this->scale);
                        $fieldQuantity = bcadd($details['trade-field_quantity'],$details['trade-reserved_quantity'],$this->scale);

                        /********** END 计算本次买入的平均金额及手续费（包含上次预留数量） ************/
                        //买入手续费
                        $runDetails['field_fees'] = bcadd($details['field_fees'],$details['trade-field_fees'],$this->scale);
                        //买入处理
                        $runDetails['amount'] = bcadd($details['amount'],$fieldAmount,$this->scale);
                        $runDetails['quantity'] = bcadd($details['quantity'],bcsub($fieldQuantity,$details['trade-field_fees'],$this->scale),$this->scale);
                        $runDetails['unit_price'] = bccomp($runDetails['quantity'],0,$this->scale) == 0?0:bcdiv($runDetails['amount'],$runDetails['quantity'],$this->scale);

                        // 止盈单价用总单价
                        $runDetails['stop_unit_price'] = $runDetails['unit_price'];

                        // 补仓单价用总单价
                        $runDetails['cover_unit_price'] = $runDetails['unit_price'];

                        if($details['trade-cover_times'] < 0) {
                            // 手动补仓
                            $runDetails['cover_times'] = $details['cover_times'] + 1;
                            $runDetails['cover_times_manual'] = $details['cover_times_manual'] + 1;
                        } else if($details['trade-cover_times'] > 0) {
                            //自动补仓
                            $runDetails['cover_times'] = $details['cover_times'] + 1;
                            $runDetails['cover_times_auto'] = $details['cover_times_auto'] + 1;
                        }

                        if($details['trade-cover_times'] >= 0) {    // 计算补仓、止盈单价
                          //  $runDetails['cover_unit_price'] = $details['trade-field_price'];    //补仓单价

                            //止盈单价
                          /*  修改计算方式：改为按总单价
                            $runDetails['stop_unit_price'] = bcmul($details['stop_unit_price'],pow(2,$details['trade-cover_times'] - 1) - 1,$this->scale);
                            $runDetails['stop_unit_price'] = bcadd($details['stop_unit_price'],bcmul($details['trade-field_price'],pow(2,$details['trade-cover_times']),$this->scale),$this->scale);
                            $runDetails['stop_unit_price'] = bcdiv($runDetails['stop_unit_price'],pow(2,$details['trade-cover_times'] + 1) - 1,$this->scale);
                          */

                        }

                    } else {    //卖出处理
                        // 卖出手续费
                        $runDetails['sell_field_fees'] = bcadd($details['sell_field_fees'],$details['trade-field_fees'],$this->scale);

                        $runDetails['sell_amount'] = bcadd($details['sell_amount'],$details['trade-field_amount'],$this->scale);
                        $runDetails['sell_quantity'] = bcadd($details['sell_quantity'],$details['trade-field_quantity'],$this->scale);
                        $runDetails['sell_price'] = bccomp($runDetails['sell_quantity'],0,$this->scale) == 0?0:bcdiv($runDetails['sell_amount'],$runDetails['sell_quantity'],$this->scale);

                        if($details['trade-statistics'] == self::JOIN_STATISTICS_LIST['NULL']) {    // 判断是否进行报表加入计算

                            // 计算盈利
                            $profit = Calculation::profit([
                                'field_price'   => $details['trade-field_price'],
                                'field_quantity'   => $details['trade-field_quantity'],
                                'reserved_quantity'   => $details['trade-reserved_quantity'],
                                'field_fees'   => $details['trade-field_fees'],
                                'unit_price'   => $details['unit_price'],
                            ]);
                            //计算服务费
                            if($serviceCharge = \app\common\model\users\Users::hasServiceCharge($details['has_service_charge'])?Calculation::serviceCharge([
                                'profit'    => $profit,
                            ],$details['trade-user_id']):0) {
                                $updateDataNew = [
                                    'statistics'    => self::JOIN_STATISTICS_LIST[bccomp($serviceCharge,0,30) == 1?'WAIT':'NOT_JOIN'],
                                    'service_charge'    => Calculation::decimal($serviceCharge),
                                    'service_charge_deducted'   => Calculation::decimal(bccomp($details['local_balance'],$serviceCharge,30) == -1?$details['local_balance']:$serviceCharge),
                                ];

                                // 判断是否添加抵扣记录
                                if(bccomp($updateDataNew['service_charge_deducted'],0,30) == 1) {
                                    // 添加抵扣记录
                                    if(!(new \app\common\model\users\Users())->consume([
                                        'id'    => $details['trade-user_id'],
                                        'local_balance' => $details['local_balance'],
                                        'local_consume' => $details['local_consume'],
                                    ],$updateDataNew['service_charge_deducted'],\app\common\model\users\Users::CONSUME_TYPE_LIST['SERVICE_CHARGE'],$details['trade-id'])) {
                                        return false;
                                    }
                                }

                            } else {
                                $updateDataNew = [
                                    'statistics'    => self::JOIN_STATISTICS_LIST[bccomp($profit,0,30) == 1?'WAIT':'NOT_JOIN']
                                ];
                            }

                            // 更新需要更新的详情信息
                            if(!$this->recordUpdate([
                                'id'    => $id,
                                'statistics' => $details['trade-statistics'],
                                'version'   => $details['trade-version']
                            ],$updateDataNew)) {
                                return false;
                            }
                        }
                    }

                    if(!(new CoinRuleRun())->recordUpdate([
                        'id'    => $details['id'],
                        'version'   => $details['version']
                    ],$runDetails)) {
                        return false;
                    }
                    /************************** END 判断修改执行状态信息  *********************************/
            }
        }
        return true;
    }


    /**
     * 编辑记录
     * @param array $where 条件
     * @param array $updateData 数据
     */
    public function recordUpdate(array $where,array $updateData) {
        $updateData = array_merge($updateData,[
            'update_time'   => time(),
            'version'   => Db::raw('if(version>250,1,version+1)'),
        ]);

        // 更新数据
        return $this->where($where)->update($updateData);
    }


    /**
     * 获取状态及条件
     * @param string $type 类型
     * @param string $tableAlias trade表别名
     * @return array
     */
    static function getStatusAndWhere(string $type,$tableAlias = '') {
        $tableAlias = empty($tableAlias)?'':($tableAlias . '.');
        $statusList = $where = [];
        switch(self::STATUS_WHERE_TYPE_LIST[$type]) {
            case self::STATUS_WHERE_TYPE_LIST['MONITOR']:
                $statusList = [
                    self::STATUS_APPLY,
                    self::STATUS_MATCH,
                    self::STATUS_PARTIAL_FILLED,
                ];
                $where[] =  [$tableAlias . 'status','between',[1,3]];
                break;
            case self::STATUS_WHERE_TYPE_LIST['FINISH']:
                $statusList = [
                    self::STATUS_CANCELING,
                    self::STATUS_FILLED,
                    self::STATUS_PARTIAL_CANCELED,
                    self::STATUS_CANCELED,
                ];
                $where[] =  [$tableAlias . 'status','between',[4,7]];
                break;
        }

        return [$statusList,$where];
    }


    /**
     * 插入申请第三方请求数据
     * @param array $details 详情
     * @return array 返回添加详情
     */
    private function apply(array $details) {
        $insertData = [
            'user_id'   => 0,
            'coin_rule_id'   => 0,
            'coin_rule_run_id'   => 0,
            'cover_times'   => 0,
            'coin_id'   => 0,
            'coin_name'   => '',
            'unit'   => $this->unitDefault,
            'type'  => 0,
            'amount'  => 0,
            'quantity'  => 0,
            'unit_price'  => 0,

            'reserved_quantity' => 0 ,  //保留数量
            'reserved_price' => 0 ,  // 保留的单价
            'sell_api_quantity' => 0,
        ];
        foreach($insertData as $key=>$val) {
            $insertData[$key] = array_key_exists($key,$details)?$details[$key]:$val;
        }

        $insertData = array_merge($insertData,[
            'order_id'  => 0,
            'client_order_id'   => $this->getClientOrderId($insertData,$details['version']),
            'field_amount'  => 0,
            'field_quantity'  => 0,
            'field_price'  => 0,
            'field_fees'  => 0,
            'request_status'  => static::REQUEST_STATUS_APPLY,
            'status'  => static::STATUS_APPLY,
            'field_time'  => 0,
            'create_time'  => time(),
            'update_time'  => time(),
        ]);

        $insertData['id'] = $this->insertGetId($insertData);
        // 返回添加详情
        return $insertData;
    }




    /**
     * 请求第三方结果处理
     * @param $tradeId 对应的ID
     * @param $requestStatus 结果状态
     * @param string $errMsg
     * @param boolean $recordErrTime 是否记录错误时间
     * @return CoinRuleRunTrade
     */
    protected function apiCallResult($tradeId,$requestStatus,$errMsg = '',string $orderId = null,bool $recordErrTime = null) {
        $isNetwork = true;
        if(is_array($errMsg)) {
            $isNetwork = $errMsg[0];
            $errMsg = $errMsg[1];
        }
        // 更新交易详情数据
        $updateData = [
            'request_status'    => $requestStatus,
            'error_msg' => substr($errMsg,0,250),
            'version'   => Db::raw('if(version>250,1,version+1)'),
            'update_time'   => time(),
        ];

        // 需要更新的执行中规则数据
        $updateDataRun = [];
        $coinRuleRunId = null;
        if($orderId) {
            $updateData['order_id'] = $orderId;
        }
        if(static::REQUEST_STATUS_SUCCESS == $requestStatus) {
            $updateData['status'] = static::STATUS_MATCH;
        } else if(!$isNetwork) {
            $updateData['status'] = static::STATUS_FAIL;
            /*********** 判断是否添加错误日期 ***********/
            if($find = $this->field(['type','coin_rule_run_id'])->find($tradeId)) {
                $find = $find->getData();
                $updateDataRun[in_array($find['type'],[static::TYPE_BUY])?'last_buy_err_time':'last_sell_err_time'] = time() - rand(0,180);
                $coinRuleRunId = $find['coin_rule_run_id'];
            }
            /********* END 判断是否添加错误日期 *********/
        }
        $result = $this->where([
            'id'    => $tradeId,
            'request_status'    => static::REQUEST_STATUS_APPLY,
        ])->update($updateData);
        if($coinRuleRunId) {    // 判断是否需要更执行中的数据
            (NEW CoinRuleRun())->recordUpdate([
                'id'    => $coinRuleRunId
            ],$updateDataRun);
        }

        return $result;
    }


    /**
     * 获取请求第三方的订单号
     * @param $details  插入详情
     * @return string 订单号详情
     */
    private function getClientOrderId($details,$version) {
        $default = time();
        $coverTimes = $details['cover_times']  > -1?$details['cover_times']:$version;
        $orderId = call_user_func_array('sprintf',[
            ($details['cover_times'] > -1?'A':'H') . '%010d%010d%05d%1d',
            $details['coin_rule_id'],
            $details['coin_rule_run_id']?$details['coin_rule_run_id']:$default,
            $coverTimes,
            $details['type'],
        ]);
/*
        $num = $this->recordAll([
                ['client_order_id','like',$orderId . '%'],
                ['status','=',static::STATUS_FAIL]
            ],0) + 1;*/

        if($recordOne = $this->recordAll([
            ['client_order_id','like',$orderId. '%'],
            ['status','=',static::STATUS_FAIL]
        ],1,['id'=>'DESC'],['client_order_id'])) {
            $num =  intval(substr($recordOne['client_order_id'],strlen($orderId))) + 1;
        } else {
            $num = 1;
        }

        $orderId = sprintf('%s%04d',$orderId,$num);
        return $orderId;
    }






}
